package com.nulldev.util.web.YouTubeExtractor.comments;

import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_CL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_LABEL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.UTC_OFFSET;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.VARIANTS_CHECKSUM;
import static com.nulldev.util.web.YouTubeExtractor.comments.handlers.ArrayCommentHandler.__process_array_based_comments;
import static com.nulldev.util.web.YouTubeExtractor.comments.handlers.CVMCommentHandler.__process_key_based_comments;
import static com.nulldev.util.web.YouTubeExtractor.comments.handlers.MobileContextCommentHandler.__process_mobile_context_comments;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getYTString;

import java.net.SocketTimeoutException;
import java.time.Duration;
import java.util.List;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.NonStandard;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.HttpClient.Version;
import com.nulldev.util.web.HttpClient.contentGen.HttpFormGenerator;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.URLUtils;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.YTIAPIClientType;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.YTIAPIClientType.YTIAPIClientEnums;
import com.nulldev.util.web.YouTubeExtractor.util.YTRequestObject;
import com.nulldev.util.web.YouTubeExtractor.util.YouTubeAmountFormatter;
import com.nulldev.util.web.legacyURI.LegacyURIUtils;

public class YTComments {
	/**
	 * Older builds (1.4.x) were based upon NewPipe's work, see below. <br>
	 * Newer builds are 100% homebrew and use reverse engineering done on YouTube's
	 * backend.
	 * 
	 * Sourced from: https://github.com/TeamNewPipe/NewPipeExtractor/blob/
	 * a2735c4f9765ff28bd967483dda2154ae3a2e8e6/extractor/src/main/java/org/schabi/
	 * newpipe/extractor/services/youtube/extractors/YoutubeCommentsExtractor.java
	 * 
	 */

	private static final Logger logger = LoggerFactory.getLoggerD(Arguments.hasArgument("--debug", "--YTComments.debug"));

	public static final boolean USE_YTAPI = Arguments.notArguments("--YTComments.useStandardClient");

	public static String find_value(final String page, final String key) {
		return find_value(page, key, 2);
	}

	public static String find_value_alt(final String page, final String key) {
		return find_value_alt(page, key, 2);
	}

	public static String find_value(final String page, final String key, final int chars) {
		final int b = page.indexOf(key) + key.length() + chars;
		final int e = page.indexOf('"', b);
		return page.substring(b, e);
	}

	public static String find_value_alt(final String page, final String key, final int chars) {
		final int b = page.indexOf(key) + key.length() + chars;
		final int e = page.indexOf('\'', b);
		return page.substring(b, e);
	}

	private static boolean isVerified(final UniJsonValue u) {
		try {
			if (Variables.requireNonNullObject(u, "u").hasValue("ownerBadges")) {
				for (final UniJsonValue badge : u.get("ownerBadges").asArray()) {
					return badge.get("metadataBadgeRenderer").getString("style").equalsIgnoreCase("BADGE_STYLE_TYPE_VERIFIED");
				}
			}
		} catch (Exception e) {
			JVM.errorln(e);
		}
		return false;
	}

	public static class YTCommentInitData {
		private volatile String commentToken;
		private final String clientName;
		private final String clientVersion;
		private final String cookiePayload;
		private final String videoID;
		private volatile boolean hasLegacyHint;
		private volatile String xsrfToken = YouTubeExtractorConstants.XSRF_TOKEN;
		private YTIAPIClientType clientType = YTIAPIClientType.WEB;

		public static YTCommentInitData make(final String videoID) throws Exception {
			return make(videoID, false);
		}

		public static YTCommentInitData make(final String videoID, final boolean forceMobile) throws Exception {
			final HttpResponse<String> o = HttpClient.getString().url("https://www.youtube.com/watch?v=" + videoID)
					.forceHeader("User-Agent", URLUtils.STANDARD_USERAGENT).timeout(Duration.ofSeconds(15)).create();
			String cookiePayload = "";
			for (final String cookie : o.cookies()) {
				cookiePayload += Splitters.splitString(cookie, ";")[0] + "; ";
			}
			if (cookiePayload.endsWith(";")) {
				cookiePayload = cookiePayload.substring(0, cookiePayload.length() - 1);
			} else if (cookiePayload.endsWith("; ")) {
				cookiePayload = cookiePayload.substring(0, cookiePayload.length() - 2);
			}
			return new YTCommentInitData(o.dataThenClose(), cookiePayload, videoID);
		}

		public YTCommentInitData(final String html, final String cookiePayload, final String videoID) {
			this.cookiePayload = cookiePayload;
			this.videoID = videoID;
			this.commentToken = StringsUtil.substringBetweenO(html, "\"reloadContinuationData\":{\"continuation\":\"", "\",\"");
			if (this.commentToken == null) {
				this.commentToken = StringsUtil.substringBetweenO(html, "{\"continuations\":[{\"nextContinuationData\":{\"continuation\":\"", "\",");
			}
			if (this.commentToken == null) {
				this.commentToken = StringsUtil.substringBetweenO(html, "\\x22nextContinuationData\\x22:\\x7b\\x22continuation\\x22:\\x22", "\\x22,\\x22");
			}
			if (this.commentToken == null) {
				this.commentToken = StringsUtil.substringBetweenO(html, "\"continuationCommand\":{\"token\":\"",
						"\",\"request\":\"CONTINUATION_REQUEST_TYPE_WATCH_NEXT\"}}}}]");
			}
			if (this.commentToken == null) {
				if (logger.debugEnabled.get()) {
					logger.debug(html);
				}
				throw new UnsupportedOperationException("Failed to find 'COMMENT_TOKEN'");
			}
			this.clientName = StringsUtil.substringBetweenO(html, "\"INNERTUBE_CONTEXT_CLIENT_NAME\":", ",\"");
			this.clientVersion = find_value(html, "INNERTUBE_CONTEXT_CLIENT_VERSION", 3);
			try {
				String s = find_value(html, "-XSRF_TOKEN", 3);
				if (s != null && !s.isEmpty())
					this.xsrfToken = StringsUtil.replace(s, "\\u003d", "=");
				s = find_value(html, "XSRF_TOKEN", 3);
				if (s != null && !s.isEmpty())
					this.xsrfToken = StringsUtil.replace(s, "\\u003d", "=");
			} catch (Exception e) {
				this.xsrfToken = YouTubeExtractorConstants.XSRF_TOKEN;
			}
		}

		public YTCommentInitData(final String videoID, final String cookiePayload, final String clientName, final String clientVersion,
				final String commentToken) {
			this.videoID = videoID;
			this.commentToken = commentToken;
			this.clientName = clientName;
			this.clientVersion = clientVersion;
			this.cookiePayload = cookiePayload;
		}

		public YTCommentInitData(final String videoID, final String cookiePayload, final String clientName, final String clientVersion,
				final String commentToken, final String xsrfToken) {
			this.videoID = videoID;
			this.commentToken = commentToken;
			this.clientName = clientName;
			this.clientVersion = clientVersion;
			this.cookiePayload = cookiePayload;
			this.xsrfToken = xsrfToken;
		}

		public YTCommentInitData(final UniJsonValue object) {
			this.videoID = object.getString("videoID");
			this.commentToken = object.getString("token", null);
			this.clientName = object.getString("clientName");
			this.clientVersion = object.getString("clientVersion");
			this.cookiePayload = object.getString("cookiePayload");
			this.xsrfToken = object.getString("xsrfToken");
			this.clientType = YTIAPIClientType.getFromID(object.getString("clientType", YTIAPIClientType.WEB.id));
			if (this.clientType == YTIAPIClientType.TVHTML5_SIMPLE_EMBEDDED_PLAYER)
				this.clientType = YTIAPIClientType.WEB;
			this.hasLegacyHint = object.bool("legacy", false);
		}

		public String getCookiePayload() {
			return this.cookiePayload;
		}

		public String getClientName() {
			return this.clientName;
		}

		public String getClientVersion() {
			return this.clientVersion;
		}

		public String getCommentToken() {
			return this.commentToken;
		}

		public String getVideoID() {
			return this.videoID;
		}

		public String getXSRFToken() {
			return this.xsrfToken;
		}

		public boolean hasLegacyHint() {
			return this.hasLegacyHint;
		}

		public YTIAPIClientType getClientType() {
			return this.clientType;
		}

		@Override
		public String toString() {
			return "YTCommentInitData[commentToken=" + this.commentToken + ",xsrfToken=" + this.xsrfToken + ",legacyHint=" + this.hasLegacyHint + ",videoID="
					+ this.videoID + ",clientName=" + this.clientName + ",clientVersion=" + this.clientVersion + "]";
		}
	}

	public static YouTubeCommentResult getComments(final YTCommentInitData initData) {
		if (initData == null) {
			throw new NullPointerException("initData == null");
		}
		return getComments(initData, initData.hasLegacyHint);
	}

	public static YouTubeCommentResult getComments(final YTCommentInitData initData, final boolean forceMobile) {
		final List<YTComment> comments = FastLists.list(20);
		if (initData == null)
			throw new NullPointerException("initData == null");
		if (initData.commentToken == null || initData.xsrfToken == null || initData.commentToken.equalsIgnoreCase("null")
				|| initData.xsrfToken.equalsIgnoreCase("null"))
			return new YouTubeCommentResult(null, comments).setCommentsDisabled(true);
		try {
			logger.debug("COMMENT_TOKEN: " + initData.commentToken);
			logger.debug("CLIENT_VERSION: " + initData.clientVersion);
			logger.debug("CLIENT_NAME: " + initData.clientName);
			logger.debug("XSRF_TOKEN: " + initData.xsrfToken);
			logger.debug("USING_YTAPI: " + USE_YTAPI);
			final YTIAPIClientType clientType = initData.clientType != null ? initData.clientType : YTIAPIClientType.WEB;
			final String CLIENT_VERSION = clientType.contains(YTIAPIClientEnums.FallbackVersion) ? clientType.get(YTIAPIClientEnums.FallbackVersion)
					: YouTubeExtractorConstants.CLIENT_VERSION;
			final String API_KEY = clientType.contains(YTIAPIClientEnums.FallbackAPIKey) ? clientType.get(YTIAPIClientEnums.FallbackAPIKey)
					: YouTubeExtractorConstants.API_KEY;
			final String forcedUserAgent = clientType.contains(YTIAPIClientEnums.CustomUserAgent) ? clientType.get(YTIAPIClientEnums.CustomUserAgent) : null;
			String url = null;
			if (USE_YTAPI) {
				url = "https://www.youtube.com/youtubei/v1/next?key=" + API_KEY + "&prettyPrint=false";
			} else {
				url = "https://www.youtube.com/comment_service_ajax" + "?action_get_comments=1" + "&pbj=1" + "&ctoken=" + initData.commentToken
						+ "&continuation=" + initData.commentToken;
			}
			String cookiePayload = initData.cookiePayload;
			logger.debug("COOKIE_PAYLOAD: " + cookiePayload);
			logger.debug("request::init -> " + url);
			HttpResponse<String> mu = null;
			if (USE_YTAPI) {
				final UniJsonValue o = YTRequestObject.makeYTRequestObject(FastMaps.fixedUniMap(new Object[][]
					{
							{ YTRequestObject.YT_REQUEST_VIDEO_ID, initData.videoID },
							{ YTRequestObject.YT_ENABLE_PLAYBACK_CONTEXT, true },
							{ YTRequestObject.YT_REQUEST_CLIENT_TYPE, clientType } }));
				o.add("continuation", initData.commentToken);
				mu = HttpClient.getString().url(url).timeout(Duration.ofSeconds(35)).forceHeader("User-Agent", forcedUserAgent)
						.header("Content-Type", "application/json").headers(new Entry[]
						{ new Entry("X-YouTube-Client-Name", clientType.get(YTIAPIClientEnums.ContextClientName)),
								new Entry("X-YouTube-Client-Version", CLIENT_VERSION), new Entry("X-YouTube-Page-CL", PAGE_CL),
								new Entry("X-YouTube-Page-Label",
										clientType.contains(YTIAPIClientEnums.IsMobileContext) ? PAGE_LABEL.replace(".desktop_", ".mobile_") : PAGE_LABEL),
								new Entry("X-YouTube-Utc-Offset", UTC_OFFSET), new Entry("X-YouTube-Variants-Checksum", VARIANTS_CHECKSUM),
								new Entry("Referer", "https://www.youtube.com/watch?v=" + initData.videoID),
								new Entry("X-SPF-Previous", "https://www.youtube.com/watch?v=" + initData.videoID),
								new Entry("X-SPF-Referer", "https://www.youtube.com/watch?v=" + initData.videoID), })
						.create(HttpMethod.POST, o.toString());
			} else {
				mu = HttpClient.getStringLegacy().url(url).timeout(Duration.ofSeconds(35)).forceHeader("User-Agent", URLUtils.STANDARD_USERAGENT)
						.header("Accept", "*/*").header("Accept-Encoding", "gzip, deflate").header("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
						.header("Cookie", cookiePayload).header("Referer", "https://www.youtube.com/watch?v=" + initData.videoID)
						.header("x-spf-previous", "https://www.youtube.com/watch?v=" + initData.videoID)
						.header("x-spf-referer", "https://www.youtube.com/watch?v=" + initData.videoID).header("x-youtube-client-name", "1")
						.header("x-youtube-client-version", initData.clientVersion).header("x-youtube-page-cl", "342016783")
						.header("x-youtube-page-label", PAGE_LABEL).header("x-youtube-time-zone", "Europe/Berlin").header("x-youtube-utc-offset", UTC_OFFSET)
						.header("x-youtube-variants-checksum", VARIANTS_CHECKSUM).preferVersion(Version.HTTP_1_1)
						.create(HttpMethod.POST, new HttpFormGenerator().add("session_token", LegacyURIUtils.safeEncode(initData.getXSRFToken())));
			}
			logger.debug("request::pre_data");
			final String resp = mu.dataThenClose();
			logger.debug("request::data");
			if (resp.isEmpty()) {
				logger.warn("initData: " + initData);
				throw new UnsupportedOperationException("Failed to get comments, RESPONSE_WAS_EMPTY!");
			} else if (resp.equalsIgnoreCase("{\"reload\":\"now\"}")) {
				logger.warn("initData: " + initData);
				throw new UnsupportedOperationException("Failed to get comments, RESPONSE_WANTED_REFRESH!");
			} else if (resp.startsWith("<!DOCTYPE")) {
				logger.warn("initData: " + initData);
				throw new UnsupportedOperationException("Failed to get comments, RESPONSE_WAS_HTML!");
			} else {
				if (clientType.contains(YTIAPIClientEnums.IsMobileContext)) {
					return __process_mobile_context_comments(resp, comments);
				} else if (resp.startsWith("[")) {
					return __process_array_based_comments(resp, comments, initData);
				} else {
					final UniJsonValue obj = UniJsonValue.parse(resp);
					if (obj == null || obj.isNull()) {
						logger.error("JSON is null!");
						return new YouTubeCommentResult(null, comments).setCommentsDisabled(true);
					}
					if (!obj.contains("onResponseReceivedEndpoints")) {
						logger.error("onResponseReceivedEndpoints object is missing!");
						obj.println();
						return new YouTubeCommentResult(null, comments).setCommentsDisabled(true);
					}
					final UniJsonValue onResponseReceivedEndpoints = obj.getSafe("onResponseReceivedEndpoints");
					if (onResponseReceivedEndpoints == null || onResponseReceivedEndpoints.isNull() || !onResponseReceivedEndpoints.isArray()) {
						logger.error("onResponseReceivedEndpoints is missing!");
						logger.info(resp);
						return new YouTubeCommentResult(null, comments).setCommentsDisabled(true);
					}
					final UniJsonValue isc = onResponseReceivedEndpoints.last().contains("appendContinuationItemsAction")
							? onResponseReceivedEndpoints.last().get("appendContinuationItemsAction")
							: onResponseReceivedEndpoints.last().get("reloadContinuationItemsCommand");
					if (isc == null || isc.isNull()) {
						logger.error("reloadContinuationItemsCommand is missing!");
						logger.info(resp);
						return new YouTubeCommentResult(null, comments).setCommentsDisabled(true);
					}
					final UniJsonValue _contents = isc.get("continuationItems");
					for (final UniJsonValue comment : Variables.requireNonNullObject(_contents, "_contents").asArray()) {
						try {
							if (!comment.contains("commentThreadRenderer") && !comment.contains("continuationItemRenderer")
									&& !comment.contains("commentRenderer") && !comment.contains("commentViewModel")) {
								logger.warn("[continuationItems] Unknown object: " + comment);
								continue;
							} else if (comment.contains("continuationItemRenderer")) {
								continue;
							} else if (comment.contains("commentViewModel")) {
								__process_key_based_comments(obj, comment, comments);
								continue;
							} else if (comment.contains("commentThreadRenderer") && comment.get("commentThreadRenderer").contains("commentViewModel")) {
								__process_key_based_comments(obj, comment, comments);
								continue;
							} else {
								final UniJsonValue o;
								final UniJsonValue r;
								try {
									if (comment.contains("commentRenderer")) {
										o = comment.get("commentRenderer");
										r = null; // Unsupported.
									} else {
										o = comment.getJSONObject("commentThreadRenderer").getJSONObject("comment").getJSONObject("commentRenderer");
										r = comment.getJSONObject("commentThreadRenderer").getJSONObject("replies");
									}
								} catch (Exception ex) {
									logger.error("Failed to get comment renderer object!");
									logger.error(comment);
									continue;
								}
								final String authorName = getYTString(o.getJSONObject("authorText"));
								final UniJsonValue c = o.getJSONObject("authorThumbnail").get("thumbnails");
								final String authorThumbnail = c.getJsonObject(c.size() - 1).getString("url");
								final String commentText = getYTString(o.getJSONObject("contentText"));
								final boolean isAuthorLiked = o.getBoolean("isLiked");
								final boolean hasCreatorHeart = __hasCreatorHeart(o);
								final long replyCount = o.getInt("replyCount", 0);
								long likeCount = 0;
								if (o.contains("voteCount")) {
									final UniJsonValue _vc = o.get("voteCount");
									final String fp = getYTString(_vc);
									if (StringsUtil.isNumber(fp)) {
										likeCount = MathUtil.safeParseL(fp, 0);
									} else
										likeCount = YouTubeAmountFormatter.format(fp);
								} else
									likeCount = o.getInt("likeCount", 0);
								String repliesCont = null;
								try {
									if (r != null) {
										final UniJsonValue commentRepliesRenderer = r.getJSONObject("commentRepliesRenderer");
										if (commentRepliesRenderer.contains("continuations")) {
											repliesCont = commentRepliesRenderer.get("continuations").getJsonObject(0).getJSONObject("nextContinuationData")
													.getString("continuation");
										} else {
											repliesCont = commentRepliesRenderer.get("contents").getJsonObject(0).getJSONObject("continuationItemRenderer")
													.getJSONObject("continuationEndpoint").getJSONObject("continuationCommand").getString("token");
										}
									}
								} catch (Exception e) {
								}
								comments.add(new YTComment(authorName, authorThumbnail, isAuthorLiked, hasCreatorHeart, o.contains("pinnedCommentBadge"),
										commentText, repliesCont, likeCount, replyCount, isVerified(o)));
							}
						} catch (Exception e) {
							logger.error("Failed to add comment!", e);
						}
					}
					String cont = null;
					try {
						cont = _contents.last().getJSONObject("continuationItemRenderer").get("continuationEndpoint").get("continuationCommand")
								.getString("token");
					} catch (Exception e) {
					}
					return new YouTubeCommentResult(cont, comments);
				}
			}
		} catch (Exception e) {
			if (e instanceof SocketTimeoutException) {
				logger.error("Failed to get comments! [error: " + e + "]");
			} else
				logger.error("Failed to get comments!", e);
			return new YouTubeCommentResult(null, comments).setCommentsDisabled(true);
		}
	}

	private static boolean __hasCreatorHeart(final UniJsonValue o) {
		if (o == null)
			return false;
		if (!o.contains("actionButtons"))
			return false;
		final UniJsonValue actionButtons = o.get("actionButtons");
		if (!actionButtons.contains("commentActionButtonsRenderer"))
			return false;
		final UniJsonValue commentActionButtonsRenderer = actionButtons.get("commentActionButtonsRenderer");
		if (!commentActionButtonsRenderer.contains("creatorHeart"))
			return false;
		if (!commentActionButtonsRenderer.get("creatorHeart").contains("creatorHeartRenderer"))
			return false;
		final UniJsonValue creatorHeartRenderer = commentActionButtonsRenderer.get("creatorHeart").get("creatorHeartRenderer");
		return creatorHeartRenderer.bool("isHearted");
	}

	public static class YouTubeCommentResult {
		private final String cont;
		private final List<YTComment> comments;
		private volatile boolean disabledComments;

		public YouTubeCommentResult(final String cont, final List<YTComment> comments) {
			this.comments = comments;
			this.cont = cont;
		}

		public YouTubeCommentResult setCommentsDisabled(final boolean value) {
			this.disabledComments = value;
			return this;
		}

		public boolean isCommentsDisabled() {
			return this.disabledComments;
		}

		public boolean hasCont() {
			return this.cont != null;
		}

		public String getCont() {
			return this.cont;
		}

		public List<YTComment> getComments() {
			return this.comments;
		}

		@NonStandard
		public void clear() {
			this.comments.clear();
		}

		@Override
		public String toString() {
			return "YouTubeCommentResult[hash=" + MathUtil.toHex(super.hashCode()) + ",comments=" + this.comments + "]";
		}
	}

	public static void main(String[] args) throws Exception {
		Arguments.setup(new String[]
			{ "--YTComments.enableDesktop" });
		YouTubeExtractor.extractor.waitFor();
//		logger.debugEnabled.set(true);
		final YTCommentInitData id = YTCommentInitData.make("OD15HZu4SWA");
		final YouTubeCommentResult d = getComments(id);
		JVM.println(d);
		ArrayUtils.printLln(d.comments);
	}
}
